Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add class parameters, flags, and privateWithin and annotations to newClass in reflect API #21880

Merged

Conversation

jchyb
Copy link
Contributor

@jchyb jchyb commented Nov 3, 2024

Instead of replacing the one newMethod we have, we instead add two more with varying complexity (similarly to how newMethod is handled). This is also so we can keep the initial newClass implementation (the one creating newClass with public empty primary constructor) intact, which despite being experiemental - already sees use in libraries and projects.

Fixes #21739 and addresses some old TODOs (from the stdlibExperimentalDefinitions.scala file).

@jchyb jchyb added the needs-minor-release This PR cannot be merged until the next minor release label Nov 3, 2024
*
* Parameters can be obtained via classSymbol.memberField
*/
@experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr], paramNames: List[String], paramTypes: List[TypeRepr], flags: Flags, privateWithin: Symbol): Symbol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to reformulate such that parents is also a function (to support param refs?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean that it should be a Symbol => List[TypeRepr] type? I have some trouble visualizing where we would need that classSymbol to define the parent TypeRepr (class Cls extends Cls.InnerCls?, is that legal?). Could you give me an example of a tree like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw class Cls extends Cls.InnerCls in the wild but in that case, Cls also had a companion object which contained InnerCls - not sure if that is possible to define right now but I'll have to look into the newModule method and see if it also needs updates

Copy link
Member

@bishabosha bishabosha Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean something like class anon(x: Int) extends Box[x.type] - I don't know the best solution, but this would be good to be able to support I assume - same with tracked eventually, but maybe that can be cleanly added later?

@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from 28cb854 to 33fbd5f Compare November 4, 2024 11:13
import quotes.reflect.*

val name = nameExpr.valueOrAbort
val parents = List(TypeTree.of[Object])
Copy link
Member

@bishabosha bishabosha Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be a test case where the parent needs an argument supplied via the constructor parameters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried doing that in the not-very-well-named tests/run-macros/newClassParamsExtendsClassParams, which generates something like this:

'{
   class `name`(idx: Int) extends Foo(idx)
   new `name`(22)
}

Or do you mean something else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that looks good

Copy link

@goshacodes goshacodes Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also such test cases could be added:

  1. Creating a trait with Symbol.newClass (is this possible?)
  2. Creating an abstract class with Symbol.newClass
  3. trait with parameter extending another trait with parameter (not sure about this)
  4. class with multiple parameters (this probably not needed)
  5. extending java class with parameter

@tgodzik tgodzik requested a review from sjrd November 6, 2024 15:51
@jchyb
Copy link
Contributor Author

jchyb commented Nov 6, 2024

To be clear, this is not ready, I did put this up since I wanted some feedback if anyone had some (Thank You @bishabosha!) and did not want to rush it last minute, that's why it a draft

@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from a839263 to 81cea85 Compare January 2, 2025 11:23
paramNames: List[String],
paramTypes: List[TypeRepr],
clsFlags: Flags,
clsPrivateWithin: Symbol

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nit comments.

Since it is public API, maybe align argument names with already existing? E.G. flags and not clsFlags, privateWithin and not clsPrivateWithin

clsPrivateWithin: Symbol,
consFlags: Flags,
consPrivateWithin: Symbol,
conParamFlags: List[List[Flags]]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constructor related arguments may have same prefix (now there are 3 constructor, cons and con) and maybe put all constructor related arguments together after everything else

@goshacodes
Copy link

Want this very much 👀

@goshacodes
Copy link

@jchyb @nicolasstucki Can I somehow help proceeding with this? Maybe contribute with a spree?

@jchyb
Copy link
Contributor Author

jchyb commented Jan 7, 2025

It's pretty much done in terms of the difficult stuff. I still need to add some test cases (your suggestions should help, thank you!), docs and perhaps more checks for flags. I planned for this for 3.7, and we still have some time for that, thus the seemingly slow progress.

@jchyb jchyb changed the title Add class term parameters, flags, and privateWithin to newClass in reflect API Add class parameters, flags, and privateWithin to newClass in reflect API Jan 9, 2025
@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from 30abd4f to 85fdd02 Compare January 10, 2025 15:00
@Gedochao
Copy link
Contributor

This is planned to be included in 3.7.0.

@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch 4 times, most recently from 3ee6271 to c385ec1 Compare February 4, 2025 12:27
@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from c385ec1 to 32f484a Compare February 17, 2025 09:34
@jchyb jchyb changed the title Add class parameters, flags, and privateWithin to newClass in reflect API Add class parameters, flags, and privateWithin to newClass and annotations in reflect API Feb 17, 2025
@jchyb jchyb requested review from hamzaremmal and smarter and removed request for sjrd February 17, 2025 09:43
@jchyb
Copy link
Contributor Author

jchyb commented Feb 17, 2025

@hamzaremmal @smarter Ideally once we update these methods we'll be able to stabilize them before the next LTS. That is to say, I would really like to merge this before the end of the current cycle, for 3.7. It might seem like a lot of lines of code, but most of those are the added tests and docs - the real changes are all in QuotesImpl.scala (other files like TreeChecker or SourceCode.scala might also have changes to better report errors related to newClass, or better print trees created with newClass).

@jchyb jchyb marked this pull request as ready for review February 17, 2025 09:52
@jchyb jchyb changed the title Add class parameters, flags, and privateWithin to newClass and annotations in reflect API Add class parameters, flags, and privateWithin and annotations to newClass in reflect API Feb 17, 2025
Copy link
Member

@hamzaremmal hamzaremmal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a first part of my review. I'm still looking at QuotesImpl and the tests. I will update the status or submit a new review when I finish.

@@ -3837,8 +3838,171 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
* direct or indirect children of the reflection context's owner.
*/
// TODO: add flags and privateWithin
@experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol
@experimental def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the name of a parameter is not something we can do as we wish, since users might be using named parameters in their code and this change can break it. However, this is an experimental method, so we are free to do this change for the time being.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To align with everything else, shouldn't we also change this?

Suggested change
@experimental def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol
@experimental def newClass(owner: Symbol, name: String, parents: Symbol => List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not related, but we could extract it into its own issue) I'm not sure how much this change can be useful but it would for sure add expressiveness.

Suggested change
@experimental def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol
@experimental def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Symbol => Option[TypeRepr]): Symbol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep that overload of newClass simple and adding this would break current Scalamock implementation (which we should not worry about too much since this method is experimental, but I just did not think it was necessary)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, as I said, it's not related, but we should consider it at some point.

@@ -1379,13 +1379,13 @@ object SourceCode {
printTypeTree(bounds.low)
else
bounds.low match {
case Inferred() =>
case Inferred() if bounds.low.tpe.typeSymbol == TypeRepr.of[Nothing].typeSymbol =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cosmetic change and it only hides when we infer Nothing (or Any for the second change). What was the motivation you had behind it?

Copy link
Contributor Author

@jchyb jchyb Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an issue that when I built a tree like class SomeClass[T <: Int] and would call .show on it, it would only render class SomeClass[T]. It turned out that trees created with tpd.TypeTree are treated as Inferred in the Quotes reflect api, and if I wanted to actually render something, I needed this change here (I figure it might be better to have a printout a bit too big, than not big enough). I recall trying to test if it was possible to adjust the reflect Inferred tree definition instead, but I do not remember what the problem there was. Test case where this matters is newClassTypeParams.

* val clsDef = ClassDef(cls, parents, body = List(fooDef))
* val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), List(idxExpr.asTerm, strExpr.asTerm))
*
* Block(List(clsDef), Apply(Select(newCls, cls.methodMember("foo")(0)), Nil)).asExprOf[Unit]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first overload and the next one have a splice version of the resulting tree in the documentation. This is extracted from the first one:

       *  constructs the equivalent to
       *   ```
       *  '{
       *    class myClass() extends Object with Foo {
       *      def foo(): Unit = println("Calling foo")
       *    }
       *    new myClass(): Foo
       *  }
       *  ```

We should also add that here imho.

parentTypes: Symbol => List[Type],
selfInfo: Type = NoType,
privateWithin: Symbol = NoSymbol,
annotations: List[Tree] = Nil,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newNormalizedClassSymbol doesn't actually have the annotations parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this one myself, we have to set annotations in the denotation, and I thought it would be convenient to do it where we set other denotation related fields

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I was just flagging one of the differences between your new method and newNormalizedClassSymbol . You shouldn't consider it as an actionable item.

/** Same as `newNormalizedClassSymbol` except that `parents` can be a function returning a list of arbitrary
* types which get normalized into type refs and parameter bindings.
*/
def newNormalizedClassSymbolUsingClassSymbolinParents(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm again not against overloading, but can't we keep the same name as newNormalizedClassSymbol. The proposed one is very (very) long...

Suggested change
def newNormalizedClassSymbolUsingClassSymbolinParents(
def newNormalizedClassSymbol(

Comment on lines 752 to 754
val parents = parentTypes(cls)
assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class")
denot.info = ClassInfo(owner.thisType, cls, parents.map(_.dealias), decls, TermRef(owner.thisType, module))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should dealias before checking if the first parent is a trait.

Suggested change
val parents = parentTypes(cls)
assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class")
denot.info = ClassInfo(owner.thisType, cls, parents.map(_.dealias), decls, TermRef(owner.thisType, module))
val parents = parentTypes(cls).map(_.dealias)
assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class")
denot.info = ClassInfo(owner.thisType, cls, parents, decls, TermRef(owner.thisType, module))

/** Same as `newNormalizedModuleSymbol` except that `parents` can be a function returning a list of arbitrary
* types which get normalized into type refs and parameter bindings.
*/
def newNormalizedModuleSymbolUsingClassSymbolInParents(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remarks here as for newNormalizedClassSymbolUsingClassSymbolinParents

Suggested change
def newNormalizedModuleSymbolUsingClassSymbolInParents(
def newNormalizedModuleSymbol(

@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from 8190f1c to 70f3eca Compare March 10, 2025 13:41
@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from 70f3eca to f4a06bd Compare March 11, 2025 14:05
@hamzaremmal hamzaremmal enabled auto-merge (squash) March 11, 2025 15:52
@jchyb jchyb force-pushed the improvement/quotes-new-class-upgrades branch from 1575dca to aaef412 Compare March 11, 2025 16:05
@hamzaremmal hamzaremmal merged commit 9e7aab7 into scala:main Mar 11, 2025
26 of 27 checks passed
@hamzaremmal hamzaremmal deleted the improvement/quotes-new-class-upgrades branch March 11, 2025 22:31
@bishabosha
Copy link
Member

@lihaoyi this should mean you can remove the hacks in Mill for Cross Module macro

@WojciechMazur WojciechMazur added the backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" label Mar 12, 2025
@WojciechMazur WojciechMazur added this to the 3.7.0 milestone Mar 12, 2025
WojciechMazur pushed a commit that referenced this pull request Mar 12, 2025
…Class in reflect API (#21880)

Instead of replacing the one newMethod we have, we instead add two more
with varying complexity (similarly to how newMethod is handled). This is
also so we can keep the initial newClass implementation (the one
creating newClass with public empty primary constructor) intact, which
despite being experiemental - already sees use in libraries and
projects.

Fixes #21739 and addresses some
old TODOs (from the stdlibExperimentalDefinitions.scala file).
@WojciechMazur WojciechMazur added backport:done This PR was successfully backported. and removed backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" labels Mar 12, 2025
@tgodzik tgodzik added the release-notes Should be mentioned in the release notes label Mar 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:done This PR was successfully backported. needs-minor-release This PR cannot be merged until the next minor release release-notes Should be mentioned in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Symbol.newClass does not support class parameters
7 participants